// 全局安装 ts ：yarn global add typescript

/**
 * 类型注解：
 * 作用：给变量添加类型约束
 * 语法：变量名: 类型
 */
// 原始类型：
const hello: string = 'hello'

const abc: boolean = true

/**
 *
 *
 *
 *
 * 数组类型
 * 作用：约束数组变量
 * 语法：
 * 1、数组变量名: 原始类型[]
 * 2、数组变量名: Array<原始类型>
 */
const arr1: number[] = [1, 2, 4]

const arr2: Array<string> = ['a', 'd', 'c']

/**
 *
 *
 *
 *
 * 联合类型：
 * 通过联合类型将多个类型合并为一个类型，例：数组中既有 string ，又有 number
 * 语法：
 * 原始变量名: 原始类型1 | 原始类型2
 * 数组变量名: (原始类型1 | 原始类型2)[]
 */
let aaa: number | string = 3
aaa = 'good'

const arr3: (number | string)[] = ['aaa', 2, 4]

/**
 *
 *
 *
 *
 * 类型别名
 * 作用：复用类型、提高类型的可复用性
 * 语法：type 类型名 = 类型
 * 场景：当多个变量是一样的类型，则可以采用类型别名
 * 注意：类型名建议使用大驼峰命名
 */
type TestType = number | string | boolean
type ArrayType = (number | string | boolean)[]

const test1: TestType = 'good'
const arr4: ArrayType = [5, '2', false]

/**
 *
 *
 *
 *
 * 函数类型
 * 解释：其实就是给 参数 和 返回值 指定类型
 * 写法：
 * 1、在函数的基础上，分别指定参数和返回值类型
 * 即： const fn1 = (a: 类型1, b: 类型2): 类型3 => {}
 * 2、使用类型别名，同时指定参数和返回值类型
 * 即： type FnType = (a: 类型1, b: 类型2) => 类型3 ，然后使用： const fn: FnType = () => {}
 *
 * 注意：
 * 1、如果函数没有返回值，则 类型3 处为 void 即可
 * 2、函数的参数为非必传项时，可以在参数后加一个 ?
 */
const fn1 = (a: number, b: string): string => {
  return a + b
}
console.log('分别指定参数和返回值类型：', fn1(10, 'vvv'))

type FnType = (num1: number, num2: number) => number
const fn2: FnType = (a, b) => {
  return a + b
}
console.log('使用类型别名，同时指定：', fn2(2, 5))

// 参数非必传：
const fn3 = (a: number, b?: string): void => {
  console.log(a, b)
}
fn3(4, 'ppp')
fn3(4)

/**
 *
 *
 *
 *
 * 对象类型
 * 语法： 变量名: { 属性名1: 类型1; 属性名2: 类型2; ... }
 */
const obj1: { name: string; age: number } = { name: '马飞', age: 21 }

// 也可以可选参数
let obj2: { name: string; age?: number; fn: () => void }
obj2 = {
  name: '哈哈',
  // age: 18,
  fn: () => {
    console.log('这是对象类型中的函数')
  }
}

// 使用类型别名：
type ObjType = { name: string; age: number; fn: () => void }
const obj3: ObjType = {
  name: '哈哈',
  age: 18,
  fn: () => {
    console.log('这是对象类型中的函数，使用类型别名')
  }
}

/**
 *
 *
 *
 *
 * 接口 interface
 * 解释：接口声明是命名对象类型的另一种方式
 * 语法：interface 接口名 { 属性名1: 类型1; 属性名2: 类型2; ... }
 *
 * 继承：
 * 使用 extends 实现接口继承，达到类型复用
 * 想同的属性可以抽离出来，然后使用 extends 实现继承复用
 * 语法： interface 接口名 extends 继承的接口名 { 属性名3: 类型3; 属性名4: 类型4; ... }
 */
interface IStudent {
  name: string
  age: number
  study: () => void
  work: (value: number) => string
}

const student1: IStudent = {
  name: '马飞',
  age: 21,
  study: () => console.log('学习中'),
  work: (money: number) => {
    return '我的工资为：' + money
  }
}

// 继承：
interface IPerson extends IStudent {
  height: number
  isGood: boolean
}

const person1: IPerson = {
  name: '马飞',
  age: 21,
  study: () => console.log('学习中'),
  work: (money: number) => {
    return '我的工资为：' + money
  },

  height: 188,
  isGood: true
}

/**
 *
 *
 *
 *
 * 交叉类型 type
 * 使用交叉类型实现接口的继承效果
 * 使用 & 可以合并连接的对象类型，也叫交叉类型
 * 语法： type 类型名 = 继承的类型名 & { 新属性名1: 类型1; 新属性名2: 类型2; ... }
 *
 * 注意：不仅可以合并类型别名，也可以合并接口 interface
 */
type Type1 = { name: string; age: number }
type Type2 = Type1 & { height: number }
type Type3 = Type1 & Type2 & { isGood: boolean }

const test2: Type2 = {
  name: '马飞',
  age: 21,
  height: 188
}

const test3: Type3 = {
  name: '马飞',
  age: 21,
  height: 188,
  isGood: false
}

/**
 *
 *
 *
 *
 * 字面量类型（了解）
 * js 字面量如：18 , 'jack' , ['s'] , { age: 10 } 等等
 * 说明：具体的值作为类型就叫做字面量类型，且值不能更改
 * 语法：变量: 字面量 = 和字面量相同的值
 */
const userName: 'good' = 'good'
const yourAge: 18 = 18
const yourArr: ['2', 5] = ['2', 5]

// 以下这种情况可以实现多个值变化
let gender: '男' | '女' = '男'
gender = '女'

// 可以应用在函数参数的选择：
type Derection = 'up' | 'down' | 'left' | 'right'
const derecFn = (derection: Derection): void => {
  console.log('字面量类型：', derection)
}
// 调用函数时，会有值的可选提示
derecFn('right')

/**
 *
 *
 *
 *
 * 类型断言
 * 有时人会比 TS 更加明确一个值的类型，此时，可以使用类型断言来指定更具体的类型
 * 解释：
 * 1、使用 as 关键字实现类型断言
 * 2、关键字 as 后面的类型是一个更加具体的类型，(HTMLAnchorElement 是 HTMLElement 的子类型)
 * 3、通过类型断言，aLink 的类型变得更加具体，这样就可以访问 a 标签特有的属性或方法了
 */

// 场景一：获取 dom 对象，指明其是一个元素
// 说明：默认通过类型推荐 => HTMLElement => 不够具体
// 查元素可在控制台（www.baidu.com 的控制台）里面查： document.querySelector('随便一个').__proto__ , 然后回车就可以出来元素类型
const dom1 = document.querySelector('#dom1') as HTMLAnchorElement
// dom1.href = 'http://www.baidu.com'

// 场景二：一个变量需要通过发请求得到数据，数值又太多的情况，不想一个个值都默认为空，而是开始定义为空对象情况
interface IObj {
  name: string
  data: number[]
}
// 原本定义为这样会报错，所以只能通过一个个值赋值为空
// const objData: IObj = {}
// 因此可以通过类型断言赋值为空对象，同时也可以保留对象里的数值类型：
const objData = {} as IObj
objData.name = 'good'

/**
 *
 *
 *
 *
 * any 类型
 * 主要为了逃避 TS 的类型约束（相当于变成 js）
 * 少用!
 */

/**
 *
 *
 *
 *
 * 泛型
 * 可以保证在安全的前提下，给别名、接口、函数等添加类型参数，从而实现复用
 *
 * 定义类型别名后加上 <类型参数> 就是泛型语法，使用时传入具体的类型即可
 * <T> 是一个变量，可以随意命名，遵循大驼峰即可
 * 和类型别名配合，在类型别名后加上泛型语法，然后类型别名内就可以使用这个类型参数
 * 泛型可以提高类型的 复用性 和 灵活性
 *
 *
 * 泛型别名：
 * 当定义类型别名的时候，某些值的类型无法确定时采用
 * 语法： type 类型别名<泛型别名1, 泛型别名2, ...> = { ... }
 *
 *
 * 泛型接口：
 * 语法： interface 接口名<泛型别名1, 泛型别名2, ...> { ... }
 *
 *
 * 泛型函数：保证函数内类型复用，且保证类型安全
 * 函数名称后加上 <T> ，<T> 是一个变量，可以随意命名，遵循大驼峰即可
 */

// 泛型别名：
type Type4<TypeFanxing1> = {
  name: string
  data: TypeFanxing1 // data 的数据类型不确定，因此采用泛型别名参数去指定
}
// 使用泛型别名后，就可以自定义传值定义类型
const fanxing1: Type4<number[]> = {
  name: '马飞',
  data: [1, 4]
}
const fanxing2: Type4<IObj> = {
  name: '马飞',
  data: {
    name: '哈哈',
    data: [1, 6]
  }
}

// 多个不确定的类型时：
type Type5<T1, T2> = {
  a: string
  b: T1
  c: T2
}
const tt: Type5<string[], number[]> = {
  a: '',
  b: ['1'],
  c: [2]
}

// 泛型接口：
interface IType5<TypeFanxing2> {
  name: string
  data: TypeFanxing2
}
const fanxing3: IType5<string[]> = {
  name: '马飞',
  data: ['1']
}

// 泛型函数
const fanxingFunc1 = <T>(id: T): T => {
  return id
}
function fanxingFunc2<T>(id: T): T {
  return id
}
fanxingFunc1<number>(12)
fanxingFunc1<string>('哈哈')
// ts 有类型推断机制，所以调用时可以不用写类型
// 如下：
fanxingFunc1(12)
fanxingFunc1('哈哈')

// 解决命名冲突（不写这句，则多个不同的 ts 文件也不能定义同一个变量名）：
export {}
